﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using Dokan;
using RemoteCode.Attributes;

namespace MirrorDrive.Operations
{
    public class MirrorDriveOperations : DokanOperations
    {
        private string _root;
        private FileHandleManager _handleManager;
        private DriveAccess _access;

        public MirrorDriveOperations(string root, DriveAccess access)
        {
            _root = root;
            _access = access;
            _handleManager = new FileHandleManager();
        }  
        private string GetPath(string filename)
        {
            return _root + filename;
        }

        private bool CheckAccess(DriveAccess accessType)
        {
            if (accessType == DriveAccess.None)
                return false;

            switch (accessType)
            {
                case DriveAccess.ReadOnly:
                    if (_access == DriveAccess.ReadOnly || _access == DriveAccess.All)
                        return true;
                    break;
                case DriveAccess.All:
                    if (_access == DriveAccess.All)
                        return true;
                    break;
            }
            return false;
        }
        
        public int CreateFile(String filename, FileAccess access, FileShare share,
            FileMode mode, FileOptions options, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            switch (access)
            {
                case FileAccess.Read:
                    if (!CheckAccess(DriveAccess.ReadOnly))
                        return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);
                    break;
                case FileAccess.ReadWrite:
                    if (!CheckAccess(DriveAccess.All))
                        return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);
                    break;
                case FileAccess.Write:
                    if (!CheckAccess(DriveAccess.All))
                        return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);
                    break;
            }

            string path = GetPath(filename);
            
            if (Directory.Exists(path))
            {
                info.IsDirectory = true;
                return DokanNet.DOKAN_SUCCESS;
            }
            else
            {
                switch (mode)
                {
                    case FileMode.CreateNew:
                        if (File.Exists(path))
                        {
                            return Logger.Error(-DokanNet.ERROR_ALREADY_EXISTS);
                        }
                        break;
                    case FileMode.Truncate:
                    case FileMode.Open:
                        if (!File.Exists(path))
                        {
                            return Logger.Error(-DokanNet.ERROR_FILE_NOT_FOUND);
                        }
                        break;
                    default:
                        break;
                }

                FileHandle handle = _handleManager.AddHandle(path);
                try
                {
                    handle.fileStream = new FileStream(path, mode, access, share, 4096, options);
                    info.Context = handle.Id;

                    return DokanNet.DOKAN_SUCCESS;
                }
                catch (Exception ex)
                {
                    _handleManager.RemoveHandle(handle);
                    return Logger.Error(ExceptionHelper.GetReturnCode(ex));
                }
            }
        }

        
        public int OpenDirectory(String filename, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.ReadOnly))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            if (Directory.Exists(GetPath(filename)))
                return DokanNet.DOKAN_SUCCESS;
            else
                return Logger.Error(-DokanNet.ERROR_PATH_NOT_FOUND);
        }

        
        public int CreateDirectory(String filename, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            string path = GetPath(filename);
            if (Directory.Exists(path))
                return Logger.Error(-DokanNet.ERROR_ALREADY_EXISTS);
            try
            {
                Directory.CreateDirectory(path);
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int Cleanup(String filename, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            string path = GetPath(filename);
            if (info.DeleteOnClose)
            {
                try
                {
                    if (!CheckAccess(DriveAccess.All))
                        return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

                    if (info.IsDirectory)
                        Directory.Delete(path);
                    else
                    {
                        CloseFile(filename, info);

                        File.Delete(path);
                    }
                }
                catch (Exception ex)
                {
                    return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
                }
            }
            return DokanNet.DOKAN_SUCCESS;
        }

        
        public int CloseFile(String filename, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (info.Context != null)
            {
                FileHandle handle = _handleManager.GetHandle(info);
                if (handle == null)
                    return Logger.Error(DokanNet.DOKAN_ERROR);

                try
                {
                    handle.Dispose();
                }
                catch (Exception ex)
                {
                    return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
                }
                finally
                {
                    _handleManager.RemoveHandle(handle);
                    GC.Collect();
                }
            }
            return DokanNet.DOKAN_SUCCESS;
        }

        
        public int ReadFile(String filename, ref Byte[] buffer, ref uint readBytes,
            long offset, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.ReadOnly))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            try
            {
                FileHandle handle = _handleManager.GetHandle(info);
                if (handle == null) 
                    return Logger.Error(DokanNet.DOKAN_ERROR);

                if (handle.fileStream == null || !handle.fileStream.CanRead) return DokanNet.DOKAN_ERROR;
                if (handle.fileStream.Position != offset) handle.fileStream.Seek(offset, SeekOrigin.Begin);
                readBytes = (uint)handle.fileStream.Read(buffer, 0, buffer.Length);

                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int WriteFile(String filename, Byte[] buffer,
            ref uint writtenBytes, long offset, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            try
            {
                FileHandle handle = _handleManager.GetHandle(info);
                if (handle == null) 
                    return Logger.Error(DokanNet.DOKAN_ERROR);

                if (handle.fileStream == null || !handle.fileStream.CanWrite) return DokanNet.DOKAN_ERROR;

                if (handle.fileStream.Position != offset) handle.fileStream.Seek(offset, SeekOrigin.Begin);
                handle.fileStream.Write(buffer, 0, buffer.Length);
                writtenBytes = (uint)buffer.Length;

                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int FlushFileBuffers(String filename, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            FileHandle handle = _handleManager.GetHandle(info);
            if (handle == null) 
                return Logger.Error(DokanNet.DOKAN_ERROR);

            try
            {
                handle.fileStream.Flush();
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }


        public int GetFileInformation(String filename, ref FileInformation fileinfo, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            string path = GetPath(filename);

            if (!CheckAccess(DriveAccess.ReadOnly))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            try
            {
                FileSystemInfo fsinfo;
                if (File.Exists(path))
                    fsinfo = new FileInfo(path);
                else if (Directory.Exists(path))
                    fsinfo = new DirectoryInfo(path);
                else return Logger.Error(DokanNet.DOKAN_ERROR);

                fileinfo = new FileInformation
                {
                    FileName = filename,
                    Attributes = fsinfo.Attributes,
                    CreationTime = fsinfo.CreationTime,
                    LastAccessTime = fsinfo.LastAccessTime,
                    LastWriteTime = fsinfo.LastWriteTime,
                    Length = (fsinfo is FileInfo) ? ((FileInfo)fsinfo).Length : 0,
                };

                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex)); ;
            }
        }


        public int FindFiles(String filename, ref ArrayList files, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.ReadOnly))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            string path = GetPath(filename);
            if (Directory.Exists(path))
            {
                try
                {
                    DirectoryInfo d = new DirectoryInfo(path);
                    FileSystemInfo[] entries = d.GetFileSystemInfos();
                    foreach (FileSystemInfo f in entries)
                    {
                        FileInformation fi = new FileInformation();
                        fi.Attributes = f.Attributes;
                        fi.CreationTime = f.CreationTime;
                        fi.LastAccessTime = f.LastAccessTime;
                        fi.LastWriteTime = f.LastWriteTime;
                        fi.Length = (f is DirectoryInfo) ? 0 : ((FileInfo)f).Length;
                        fi.FileName = f.Name;
                        files.Add(fi);
                    }
                }
                catch (Exception ex)
                {
                    return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
                }

                return DokanNet.DOKAN_SUCCESS;
            }
            else return Logger.Error(-DokanNet.ERROR_PATH_NOT_FOUND);
        }        


        public int SetFileAttributes(String filename, FileAttributes attr, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.ReadOnly))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            try
            {
                File.SetAttributes(GetPath(filename), attr);
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int SetFileTime(String filename, DateTime ctime,
                DateTime atime, DateTime mtime, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            string path = GetPath(filename);

            try
            {
                if (ctime != null && ctime.Year > 1)
                    File.SetCreationTime(path, ctime);
                if (atime != null && atime.Year > 1)
                    File.SetLastAccessTime(path, atime);
                if (mtime != null && mtime.Year > 1)
                    File.SetLastWriteTime(path, mtime);

                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int DeleteFile(String filename, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            string path = GetPath(filename);

            if (!File.Exists(path))
                return Logger.Error(-DokanNet.ERROR_FILE_NOT_FOUND);

            return DokanNet.DOKAN_SUCCESS;
        }

        
        public int DeleteDirectory(String filename, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            string path = GetPath(filename);

            if (!Directory.Exists(path))
                return Logger.Error(-DokanNet.ERROR_PATH_NOT_FOUND);

            if (Directory.GetFileSystemEntries(path).Length > 0)
                return -DokanNet.ERROR_DIR_NOT_EMPTY;

            return DokanNet.DOKAN_SUCCESS;
        }

        
        public int MoveFile(String filename, String newname, bool replace, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            string path = GetPath(filename);
            string newPath = GetPath(newname);

            if (!File.Exists(path)) return Logger.Error(-DokanNet.ERROR_FILE_NOT_FOUND);
            if (!replace && File.Exists(newPath)) return Logger.Error(-DokanNet.ERROR_ALREADY_EXISTS);

            try
            {
                if (replace && File.Exists(newPath)) File.Delete(newPath);
                File.Move(path, newPath);
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int SetEndOfFile(String filename, long length, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            FileHandle handle = _handleManager.GetHandle(info);
            if (handle == null) return Logger.Error(DokanNet.DOKAN_ERROR);

            try
            {
                handle.fileStream.SetLength(length);
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int SetAllocationSize(String filename, long length, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            FileHandle handle = _handleManager.GetHandle(info);
            if (handle == null) 
                return Logger.Error(DokanNet.DOKAN_ERROR);

            try
            {
                handle.fileStream.SetLength(length);
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int LockFile(String filename, long offset, long length, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            FileHandle handle = _handleManager.GetHandle(info);
            if (handle == null) 
                return Logger.Error(DokanNet.DOKAN_ERROR);

            try
            {
                if (length > 0)
                    handle.fileStream.Lock(offset, length);
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int UnlockFile(String filename, long offset, long length, DokanFileInfo info)
        {
            Logger.DisplayCall(filename, info, _root);

            if (!CheckAccess(DriveAccess.All))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            FileHandle handle = _handleManager.GetHandle(info);
            if (handle == null) 
                return Logger.Error(DokanNet.DOKAN_ERROR);

            try
            {
                if (length > 0)
                    handle.fileStream.Unlock(offset, length);
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int GetDiskFreeSpace(ref ulong freeBytesAvailable, ref ulong totalBytes,
            ref ulong totalFreeBytes, DokanFileInfo info)
        {
            Logger.DisplayCall(_root, info, _root);

            if (!CheckAccess(DriveAccess.ReadOnly))
                return Logger.Error(-DokanNet.ERROR_ACCESS_DENIED);

            try
            {
                string driveChar = _root.Substring(0, _root.IndexOf(":"));
                DriveInfo[] drives = DriveInfo.GetDrives();
                for (int i = 0; i < drives.Length; i++)
                    if (drives[i].Name == driveChar + ":\\")
                    {
                        freeBytesAvailable = (ulong)drives[i].AvailableFreeSpace;
                        totalBytes = (ulong)drives[i].TotalSize;
                        totalFreeBytes = (ulong)drives[i].TotalFreeSpace;
                        break;
                    }
                return DokanNet.DOKAN_SUCCESS;
            }
            catch (Exception ex)
            {
                return Logger.Error(ExceptionHelper.GetReturnCode(ex));;
            }
        }

        
        public int Unmount(DokanFileInfo info)
        {
            Logger.DisplayCall(_root, info, _root);

            _handleManager.Clean();

            return DokanNet.DOKAN_SUCCESS;
        }
    }
    public enum DriveAccess
    {
        ReadOnly,
        All,
        None
    }
}
